package com.tsd.core.utils;

import cn.hutool.db.Db;
import cn.hutool.db.DbUtil;
import cn.hutool.db.Entity;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.tsd.core.constants.Constants;
import com.tsd.core.datasource.DataSourceContextHolder;
import com.tsd.core.datasource.DataSourceUtils;
import org.apache.commons.io.FileUtils;
import org.flywaydb.core.Flyway;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * @ClassName: FlywayUtil
 * @Description: TODO
 * @Author: Hillpool
 * @Date: 2022/9/9 13:43
 * @Version: 1.0
 */
public class FlywayUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(FlywayUtil.class);

    private static final String CLASSPATH = "classpath*:db/migration/**/*.sql";
    private static final String BASE_PATH = "/db/migration/";
    private static final String SEPARATOR = "_";
    private static final String VERSION_SEPARATOR = "__";

    private static final List<File> JAR_FILE;

    static {
        JAR_FILE = new ArrayList<>();
    }

    private static String getBasePath() {
        String basePath = Objects.requireNonNull(FlywayUtil.class.getResource("/")).getPath();
        if (!basePath.endsWith("/")) {
            basePath += BASE_PATH.replaceFirst("/", "");
        } else {
            basePath += BASE_PATH;
        }
        return basePath;
    }

    /**
     * 从jar包复制Flyway迁移脚本
     */
    public static void copyJarFlyway2Classpath() {
        JAR_FILE.clear();
        String basePath = getBasePath();
        List<File> fileList = getSqlFile(basePath);
        List<String> existFileList = ListUtil.map(fileList, File::getName);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] sqlResources;
        try {
            sqlResources = resolver.getResources(CLASSPATH);
            for (Resource resource : sqlResources) {
                String path = resource.getURL().getPath();
                if (!existFileList.contains(resource.getFilename())) {
                    String[] paths = path.split(BASE_PATH);
                    if (paths.length == 2) {
                        String filePath = basePath + paths[1];
                        File sql = new File(filePath);
                        FileUtils.copyInputStreamToFile(resource.getInputStream(), sql);
                        JAR_FILE.add(sql);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 清除从jar包复制的Flyway迁移脚本
     */
    public static void clearJarFlywayFromClasspath() {
        if (HlpUtils.isEmptyList(JAR_FILE)) {
            return;
        }
        for (File file : JAR_FILE) {
            if (file.exists()) {
                FileUtil.delete(file.getAbsolutePath());
            }
        }
        JAR_FILE.clear();
        // 清除空目录
        String basePath = getBasePath();
        File filePath = new File(basePath);
        if (!filePath.exists()) {
            return;
        }
        File[] paths = filePath.listFiles();
        if (paths != null) {
            for (File file : paths) {
                File[] children = file.listFiles();
                if (file.isDirectory() && (children == null || children.length == 0)) {
                    FileUtil.deleteDir(file);
                }
            }
        }
    }

    private static List<File> getSqlFile(String path) {
        List<File> fileList = new ArrayList<>();
        try {
            if (path == null) {
                path = Objects.requireNonNull(FlywayUtil.class.getClassLoader().getResource(BASE_PATH)).getPath();
                if (!FileUtil.ifExists(path)) {
                    return fileList;
                }
            }
            String[] strings = FileUtil.getFileList(path);
            if (strings != null) {
                for (String string : strings) {
                    StringBuilder pathBuilder = new StringBuilder(path);
                    if (!pathBuilder.toString().endsWith("/") && !pathBuilder.toString().endsWith("\\")) {
                        pathBuilder.append(File.separator);
                    }
                    pathBuilder.append(string);
                    File file = new File(pathBuilder.toString());
                    if (file.isDirectory()) {
                        List<File> list = getSqlFile(file.getAbsolutePath());
                        if (!HlpUtils.isEmptyList(list)) {
                            fileList.addAll(list);
                        }
                        continue;
                    }
                    if (file.isFile() && file.getName().endsWith(".sql")) {
                        fileList.add(file);
                    }
                }
            }
        } catch (Exception ignored) {
        }
        return fileList;
    }

    private static String getNewName(String name) {
        String[] strings = name.split(VERSION_SEPARATOR);
        if (strings.length == 2) {
            String str = HlpUtils.date2String(new Date(), HlpUtils.Format_YyyyMMddHHmmssSSS);
            if (name.startsWith("V")) {
                return "V" + str + VERSION_SEPARATOR + name;
            }
            if (name.startsWith("R")) {
                return "R" + VERSION_SEPARATOR + str + SEPARATOR + name;
            }
        }
        return null;
    }

    private static List<String> getExistRunSql(DataSource dataSource) {
        List<String> runList = new ArrayList<>();
        boolean tableExist = DataSourceUtils.isTableExist(dataSource, Constants.TABLE_NAME_FLYWAY_SCHEMA_HISTORY);
        if (tableExist) {
            Db db = DbUtil.use(dataSource);
            try {
                // 删除执行失败的记录
                String delSql = "DELETE FROM `flyway_schema_history` WHERE `type` = 'SQL' AND `success` = 0";
                db.execute(delSql);
                // 查询已执行成功的记录
                String sql = "SELECT `script` FROM `flyway_schema_history` WHERE `type` = 'SQL' AND `success` = 1 ORDER BY `installed_rank`";
                List<Entity> list = db.query(sql);
                if (!HlpUtils.isEmptyList(list)) {
                    for (Entity entity : list) {
                        String script = entity.getStr("script");
                        if (!HlpUtils.isEmpty(script) && script.startsWith("V")) {
                            String[] strs = script.split(VERSION_SEPARATOR);
                            if (strs.length == 3) {
                                runList.add(strs[1] + VERSION_SEPARATOR + strs[2]);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                LOGGER.error(ExceptionUtil.getStackTraceFromException(e));
            }
        }
        DataSourceContextHolder.setDataSourceType(null);
        return runList;
    }

    private static List<File> filterValidSqlFile(List<File> fileList, Entity entity) {
        List<String> list = new ArrayList<>();
        String modeStr = entity.getStr("mode");
        if (!HlpUtils.isEmpty(modeStr) && JSONUtil.isJsonArray(modeStr)) {
            List<String> modeList = JSON.parseArray(modeStr, String.class);
            modeList = ListUtil.filter(modeList, t -> !"tsd".equals(t));
            if (!HlpUtils.isEmptyList(modeList)) {
                for (String mode : modeList) {
                    list.add(BASE_PATH + mode);
                }
            }
        }
        return ListUtil.filter(fileList, t -> {
            String filePath = t.getAbsolutePath().replaceAll("\\\\", "/");
            String validPath = ListUtil.get(list, filePath::contains);
            return !HlpUtils.isEmpty(validPath);
        });
    }

    private static boolean mkdirs(String path) {
        boolean success = true;
        File file = new File(path);
        if (!file.exists()) {
            try {
                success = file.mkdirs();
            } catch (Exception e) {
                e.printStackTrace();
                success = false;
            }
        }
        return success;
    }

    /**
     * 根据系统账套中的"模式"组装sql文件
     *
     * @param dataSource
     * @param entity
     */
    public static void procSqlFile2AccountSet4Mode(DataSource dataSource, Entity entity) {
        // 1、生成目录
        String basePath = Objects.requireNonNull(FlywayUtil.class.getResource(BASE_PATH)).getPath();
        if (!basePath.endsWith("/") && !basePath.endsWith("\\")) {
            basePath += File.separator;
        }
        String path = basePath + "account_" + entity.getStr("sid");
        boolean success = mkdirs(path);
        if (!success) {
            return;
        }
        // 2、自动获取需要的sql文件
        List<File> allFileList = getSqlFile(basePath);
        // 提取tsd基础文件
        List<File> tsdFileList = ListUtil.filter(allFileList, t -> {
            String[] strings = t.getAbsolutePath().replace("\\", "/").split("/db/migration/");
            if (strings.length == 2) {
                return strings[1].startsWith("tsd/");
            }
            return false;
        });
        tsdFileList = ListUtil.sorted(tsdFileList, Comparator.comparing(File::getName));
        // 过滤每次都执行的文件
        List<File> rFileList = ListUtil.filter(allFileList, t -> t.getName().startsWith("R"));
        // 过滤只执行一次的文件
        List<File> vFileList = ListUtil.filter(allFileList, t -> t.getName().startsWith("V"));
        // 3、根据模式过滤目录
        vFileList = filterValidSqlFile(vFileList, entity);
        // 4、过滤已执行成功的文件
        List<String> existRunList = getExistRunSql(dataSource);
        if (!HlpUtils.isEmptyList(existRunList)) {
            vFileList = ListUtil.filter(vFileList, t -> {
                String existRunSql = ListUtil.get(existRunList, it -> t.getName().equals(it));
                return HlpUtils.isEmpty(existRunSql);
            });
            tsdFileList = ListUtil.filter(tsdFileList, t -> {
                String existRunSql = ListUtil.get(existRunList, it -> t.getName().equals(it));
                return HlpUtils.isEmpty(existRunSql);
            });
        }
        boolean isLog = entity.containsKey("log") && entity.getBool("log");
        LinkedList<File> fileList = new LinkedList<>();
        if (!HlpUtils.isEmptyList(tsdFileList) && !isLog) {
            fileList.addAll(tsdFileList);
        }
        if (!HlpUtils.isEmptyList(vFileList)) {
            vFileList = ListUtil.sorted(vFileList, Comparator.comparing(File::getName));
            fileList.addAll(vFileList);
        }
        if (!HlpUtils.isEmptyList(rFileList) && !isLog) {
            rFileList = ListUtil.sorted(rFileList, Comparator.comparing(File::getName));
            fileList.addAll(rFileList);
        }
        if (HlpUtils.isEmptyList(fileList)) {
            return;
        }
        for (File sqlFile : fileList) {
            String newName = getNewName(sqlFile.getName());
            if (HlpUtils.isEmpty(newName)) {
                continue;
            }
            String dist = path + File.separator + newName;
            try {
                FileUtil.copyFile(sqlFile.getAbsolutePath(), dist, true);
                // 阻塞，避免版本号重复
                Thread.sleep(1);
            } catch (Exception e) {
                LOGGER.error(ExceptionUtil.getStackTraceFromException(e));
            }
        }
    }

    /**
     * 清除sql文件
     *
     * @param entity
     */
    public static void clearSqlFile(Entity entity) {
        String basePath = Objects.requireNonNull(FlywayUtil.class.getResource(BASE_PATH)).getPath();
        if (!basePath.endsWith("/") && !basePath.endsWith("\\")) {
            basePath += File.separator;
        }
        String path = basePath + "account_" + entity.getStr("sid");
        File file = new File(path);
        if (file.exists()) {
            FileUtil.deleteDir(file);
        }
    }

    public static void doFlyway(DataSource dataSource, String... locations) {
        if (HlpUtils.isEmpty(locations)) {
            return;
        }
        try {
            Flyway flyway = Flyway.configure()
                    .dataSource(dataSource)
                    // 当迁移时发现目标schema非空，而且带有没有元数据的表时，是否自动执行基准迁移
                    .baselineOnMigrate(true)
                    // 迁移时是否校验
                    .validateOnMigrate(false)
                    // 当读取元数据表时是否忽略错误的迁移
                    .ignoreFutureMigrations(false)
                    .cleanDisabled(false)
                    .cleanOnValidationError(false)
                    .placeholderReplacement(false)
                    .locations(locations)
                    .load();
            flyway.migrate();
        } catch (Exception e) {
            LOGGER.error(ExceptionUtil.getStackTraceFromException(e));
        }
    }

    public static void clearExistRunSql4DataInit(DataSource dataSource, List<String> locations) {
        if (HlpUtils.isEmpty(locations)) {
            return;
        }
        boolean tableExist = DataSourceUtils.isTableExist(dataSource, Constants.TABLE_NAME_FLYWAY_SCHEMA_HISTORY);
        if (!tableExist) {
            return;
        }
        Db db = DbUtil.use(dataSource);
        // 删除已执行的记录
        String delSql = "DELETE FROM `flyway_schema_history` WHERE `type` = 'SQL' AND `script` LIKE '%s'";
        for (String location : locations) {
            List<String> paths = ResourcesUtil.getClassPath(location);
            if (HlpUtils.isEmptyList(paths)) {
                continue;
            }
            List<File> allFileList = getSqlFile(paths.get(0));
            for (File file : allFileList) {
                String script = "%" + file.getName();
                try {
                    db.execute(String.format(delSql, script));
                } catch (Exception e) {
                    LOGGER.error(ExceptionUtil.getStackTraceFromException(e));
                }
            }
        }
    }

    private static String createDbInitPath(String pathName, boolean isClear) {
        // 1、生成目录
        String basePath = Objects.requireNonNull(FlywayUtil.class.getResource("/")).getPath();
        if (!basePath.endsWith("/") && !basePath.endsWith("\\")) {
            basePath += File.separator;
        }
        String path = basePath + pathName;
        boolean success = mkdirs(path);
        if (!success) {
            return null;
        }
        if (isClear) {
            File file = new File(path);
            if (file.exists()) {
                FileUtil.deleteDir(file);
            }
        }
        return path;
    }

    private static void copyDbInitFile(Entity entity, String path, String... locations) {
        if (HlpUtils.isEmpty(locations)) {
            return;
        }
        for (String location : locations) {
            List<String> paths = ResourcesUtil.getClassPath(location);
            if (HlpUtils.isEmptyList(paths)) {
                continue;
            }
            List<File> allFileList = getSqlFile(paths.get(0));
            for (File sqlFile : allFileList) {
                String newName = getNewName(sqlFile.getName());
                if (HlpUtils.isEmpty(newName)) {
                    continue;
                }
                String dist = path + File.separator + newName;
                try {
                    FileUtil.copyFile(sqlFile.getAbsolutePath(), dist, true);
                    // 阻塞，避免版本号重复
                    Thread.sleep(1);
                } catch (Exception e) {
                    LOGGER.error(ExceptionUtil.getStackTraceFromException(e));
                }
            }
        }
    }

    public static void doFlyway4DbInit(DataSource dataSource, Entity entity, String... locations) {
        if (HlpUtils.isEmpty(locations)) {
            return;
        }
        String pathName = "db-init_" + entity.getStr("sid");
        String path = createDbInitPath(pathName, false);
        copyDbInitFile(entity, path, locations);
        doFlyway(dataSource, String.format("classpath:%s", pathName));
        createDbInitPath(pathName, true);
    }
}
